﻿Option Strict On

Imports System.Text
Imports System.Net
Imports System.Net.Sockets
Imports System.Globalization
Imports System.IO
Imports System.Threading
Imports System.ComponentModel

Namespace Proxy
    ''' <summary>
    ''' Socks4 connection proxy class.  This class implements the Socks4 standard proxy protocol.
    ''' </summary>
    ''' <remarks>
    ''' This class implements the Socks4 proxy protocol standard for TCP communciations.
    ''' </remarks>
    Public Class Socks4ProxyClient
        Implements IProxyClient
        Private Const WAIT_FOR_DATA_INTERVAL As Integer = 50
        ' 50 ms
        Private Const WAIT_FOR_DATA_TIMEOUT As Integer = 15000
        ' 15 seconds
        Private Const PROXY_NAME As String = "SOCKS4"
        Private _tcpClient As TcpClient

        Private _proxyHost As String
        Private _proxyPort As Integer
        Private _proxyUserId As String

        ''' <summary>
        ''' Default Socks4 proxy port.
        ''' </summary>
        Friend Const SOCKS_PROXY_DEFAULT_PORT As Integer = 1080
        ''' <summary>
        ''' Socks4 version number.
        ''' </summary>
        Friend Const SOCKS4_VERSION_NUMBER As Byte = 4
        ''' <summary>
        ''' Socks4 connection command value.
        ''' </summary>
        Friend Const SOCKS4_CMD_CONNECT As Byte = &H1
        ''' <summary>
        ''' Socks4 bind command value.
        ''' </summary>
        Friend Const SOCKS4_CMD_BIND As Byte = &H2
        ''' <summary>
        ''' Socks4 reply request grant response value.
        ''' </summary>
        Friend Const SOCKS4_CMD_REPLY_REQUEST_GRANTED As Byte = 90
        ''' <summary>
        ''' Socks4 reply request rejected or failed response value.
        ''' </summary>
        Friend Const SOCKS4_CMD_REPLY_REQUEST_REJECTED_OR_FAILED As Byte = 91
        ''' <summary>
        ''' Socks4 reply request rejected becauase the proxy server can not connect to the IDENTD server value.
        ''' </summary>
        Friend Const SOCKS4_CMD_REPLY_REQUEST_REJECTED_CANNOT_CONNECT_TO_IDENTD As Byte = 92
        ''' <summary>
        ''' Socks4 reply request rejected because of a different IDENTD server.
        ''' </summary>
        Friend Const SOCKS4_CMD_REPLY_REQUEST_REJECTED_DIFFERENT_IDENTD As Byte = 93

        ''' <summary>
        ''' Create a Socks4 proxy client object.  The default proxy port 1080 is used.
        ''' </summary>
        Public Sub New()
        End Sub

        ''' <summary>
        ''' Creates a Socks4 proxy client object using the supplied TcpClient object connection.
        ''' </summary>
        ''' <param name="tcpClient">A TcpClient connection object.</param>
        Public Sub New(ByVal tcpClient As TcpClient)
            If tcpClient Is Nothing Then
                Throw New ArgumentNullException("tcpClient")
            End If

            _tcpClient = tcpClient
        End Sub

        ''' <summary>
        ''' Create a Socks4 proxy client object.  The default proxy port 1080 is used.
        ''' </summary>
        ''' <param name="proxyHost">Host name or IP address of the proxy server.</param>
        ''' <param name="proxyUserId">Proxy user identification information.</param>
        Public Sub New(ByVal proxyHost As String, ByVal proxyUserId As String)
            If String.IsNullOrEmpty(proxyHost) Then
                Throw New ArgumentNullException("proxyHost")
            End If

            If proxyUserId Is Nothing Then
                Throw New ArgumentNullException("proxyUserId")
            End If

            _proxyHost = proxyHost
            _proxyPort = SOCKS_PROXY_DEFAULT_PORT
            _proxyUserId = proxyUserId
        End Sub

        ''' <summary>
        ''' Create a Socks4 proxy client object.
        ''' </summary>
        ''' <param name="proxyHost">Host name or IP address of the proxy server.</param>
        ''' <param name="proxyPort">Port used to connect to proxy server.</param>
        ''' <param name="proxyUserId">Proxy user identification information.</param>
        Public Sub New(ByVal proxyHost As String, ByVal proxyPort As Integer, ByVal proxyUserId As String)
            If String.IsNullOrEmpty(proxyHost) Then
                Throw New ArgumentNullException("proxyHost")
            End If

            If proxyPort <= 0 OrElse proxyPort > 65535 Then
                Throw New ArgumentOutOfRangeException("proxyPort", "port must be greater than zero and less than 65535")
            End If

            If proxyUserId Is Nothing Then
                Throw New ArgumentNullException("proxyUserId")
            End If

            _proxyHost = proxyHost
            _proxyPort = proxyPort
            _proxyUserId = proxyUserId
        End Sub

        ''' <summary>
        ''' Create a Socks4 proxy client object.  The default proxy port 1080 is used.
        ''' </summary>
        ''' <param name="proxyHost">Host name or IP address of the proxy server.</param>
        Public Sub New(ByVal proxyHost As String)
            If String.IsNullOrEmpty(proxyHost) Then
                Throw New ArgumentNullException("proxyHost")
            End If

            _proxyHost = proxyHost
            _proxyPort = SOCKS_PROXY_DEFAULT_PORT
        End Sub

        ''' <summary>
        ''' Create a Socks4 proxy client object.
        ''' </summary>
        ''' <param name="proxyHost">Host name or IP address of the proxy server.</param>
        ''' <param name="proxyPort">Port used to connect to proxy server.</param>
        Public Sub New(ByVal proxyHost As String, ByVal proxyPort As Integer)
            If String.IsNullOrEmpty(proxyHost) Then
                Throw New ArgumentNullException("proxyHost")
            End If

            If proxyPort <= 0 OrElse proxyPort > 65535 Then
                Throw New ArgumentOutOfRangeException("proxyPort", "port must be greater than zero and less than 65535")
            End If

            _proxyHost = proxyHost
            _proxyPort = proxyPort
        End Sub

        ''' <summary>
        ''' Gets or sets host name or IP address of the proxy server.
        ''' </summary>
        Public Property ProxyHost() As String Implements SFDL.Proxy.Proxy.IProxyClient.ProxyHost
            Get
                Return _proxyHost
            End Get
            Set(ByVal value As String)
                _proxyHost = value
            End Set
        End Property

        ''' <summary>
        ''' Gets or sets port used to connect to proxy server.
        ''' </summary>
        Public Property ProxyPort() As Integer Implements SFDL.Proxy.Proxy.IProxyClient.ProxyPort
            Get
                Return _proxyPort
            End Get
            Set(ByVal value As Integer)
                _proxyPort = value
            End Set
        End Property

        ''' <summary>
        ''' Gets String representing the name of the proxy. 
        ''' </summary>
        ''' <remarks>This property will always return the value 'SOCKS4'</remarks>
        Public Overridable ReadOnly Property ProxyName() As String Implements SFDL.Proxy.Proxy.IProxyClient.ProxyName
            Get
                Return PROXY_NAME
            End Get
        End Property

        ''' <summary>
        ''' Gets or sets proxy user identification information.
        ''' </summary>
        Public Property ProxyUserId() As String
            Get
                Return _proxyUserId
            End Get
            Set(ByVal value As String)
                _proxyUserId = value
            End Set
        End Property

        ''' <summary>
        ''' Gets or sets the TcpClient object. 
        ''' This property can be set prior to executing CreateConnection to use an existing TcpClient connection.
        ''' </summary>
        Public Property TcpClient() As TcpClient Implements SFDL.Proxy.Proxy.IProxyClient.TcpClient
            Get
                Return _tcpClient
            End Get
            Set(ByVal value As TcpClient)
                _tcpClient = value
            End Set
        End Property

        ''' <summary>
        ''' Creates a TCP connection to the destination host through the proxy server
        ''' host.
        ''' </summary>
        ''' <param name="destinationHost">Destination host name or IP address of the destination server.</param>
        ''' <param name="destinationPort">Port number to connect to on the destination server.</param>
        ''' <return>
        ''' return an open TcpClient object that can be used normally to communicate
        ''' with the destination server
        ''' </return>
        ''' <remarks>
        ''' This method creates a connection to the proxy server and instructs the proxy server
        ''' to make a pass through connection to the specified destination host on the specified
        ''' port.  
        ''' </remarks>
        Public Function CreateConnection(ByVal destinationHost As String, ByVal destinationPort As Integer) As TcpClient Implements SFDL.Proxy.Proxy.IProxyClient.CreateConnection
            If String.IsNullOrEmpty(destinationHost) Then
                Throw New ArgumentNullException("destinationHost")
            End If

            If destinationPort <= 0 OrElse destinationPort > 65535 Then
                Throw New ArgumentOutOfRangeException("destinationPort", "port must be greater than zero and less than 65535")
            End If

            Try
                ' if we have no connection, create one
                If _tcpClient Is Nothing Then
                    If String.IsNullOrEmpty(_proxyHost) Then
                        Throw New ProxyException("ProxyHost property must contain a value.")
                    End If

                    If _proxyPort <= 0 OrElse _proxyPort > 65535 Then
                        Throw New ProxyException("ProxyPort value must be greater than zero and less than 65535")
                    End If

                    '  create new tcp client object to the proxy server
                    _tcpClient = New TcpClient()

                    ' attempt to open the connection
                    _tcpClient.Connect(_proxyHost, _proxyPort)
                End If

                '  send connection command to proxy host for the specified destination host and port
                SendCommand(_tcpClient.GetStream(), SOCKS4_CMD_CONNECT, destinationHost, destinationPort, _proxyUserId)

                ' return the open proxied tcp client object to the caller for normal use
                Return _tcpClient
            Catch ex As Exception
                Throw New ProxyException(String.Format(CultureInfo.InvariantCulture, "Connection to proxy host {0} on port {1} failed.", Utils.GetHost(_tcpClient), Utils.GetPort(_tcpClient)), ex)
            End Try
        End Function


        ''' <summary>
        ''' Sends a command to the proxy server.
        ''' </summary>
        ''' <param name="proxy">Proxy server data stream.</param>
        ''' <param name="command">Proxy byte command to execute.</param>
        ''' <param name="destinationHost">Destination host name or IP address.</param>
        ''' <param name="destinationPort">Destination port number</param>
        ''' <param name="userId">IDENTD user ID value.</param>
        Friend Overridable Sub SendCommand(ByVal proxy As NetworkStream, ByVal command As Byte, ByVal destinationHost As String, ByVal destinationPort As Integer, ByVal userId As String)
            ' PROXY SERVER REQUEST
            ' The client connects to the SOCKS server and sends a CONNECT request when
            ' it wants to establish a connection to an application server. The client
            ' includes in the request packet the IP address and the port number of the
            ' destination host, and userid, in the following format.
            '
            '        +----+----+----+----+----+----+----+----+----+----+....+----+
            '        | VN | CD | DSTPORT |      DSTIP        | USERID       |NULL|
            '        +----+----+----+----+----+----+----+----+----+----+....+----+
            ' # of bytes:	   1    1      2              4           variable       1
            '
            ' VN is the SOCKS protocol version number and should be 4. CD is the
            ' SOCKS command code and should be 1 for CONNECT request. NULL is a byte
            ' of all zero bits.         

            '  userId needs to be a zero length string so that the GetBytes method
            '  works properly
            If userId Is Nothing Then
                userId = ""
            End If

            Dim destIp As Byte() = GetIPAddressBytes(destinationHost)
            Dim destPort As Byte() = GetDestinationPortBytes(destinationPort)
            Dim userIdBytes As Byte() = ASCIIEncoding.ASCII.GetBytes(userId)
            Dim request As Byte() = New Byte(9 + (userIdBytes.Length - 1)) {}

            '  set the bits on the request byte array
            request(0) = SOCKS4_VERSION_NUMBER
            request(1) = command
            destPort.CopyTo(request, 2)
            destIp.CopyTo(request, 4)
            userIdBytes.CopyTo(request, 8)
            request(8 + userIdBytes.Length) = &H0
            ' null (byte with all zeros) terminator for userId
            ' send the connect request
            proxy.Write(request, 0, request.Length)

            ' wait for the proxy server to respond
            WaitForData(proxy)

            ' PROXY SERVER RESPONSE
            ' The SOCKS server checks to see whether such a request should be granted
            ' based on any combination of source IP address, destination IP address,
            ' destination port number, the userid, and information it may obtain by
            ' consulting IDENT, cf. RFC 1413.  If the request is granted, the SOCKS
            ' server makes a connection to the specified port of the destination host.
            ' A reply packet is sent to the client when this connection is established,
            ' or when the request is rejected or the operation fails. 
            '
            '        +----+----+----+----+----+----+----+----+
            '        | VN | CD | DSTPORT |      DSTIP        |
            '        +----+----+----+----+----+----+----+----+
            ' # of bytes:	   1    1      2              4
            '
            ' VN is the version of the reply code and should be 0. CD is the result
            ' code with one of the following values:
            '
            '    90: request granted
            '    91: request rejected or failed
            '    92: request rejected becuase SOCKS server cannot connect to
            '        identd on the client
            '    93: request rejected because the client program and identd
            '        report different user-ids
            '
            ' The remaining fields are ignored.
            '
            ' The SOCKS server closes its connection immediately after notifying
            ' the client of a failed or rejected request. For a successful request,
            ' the SOCKS server gets ready to relay traffic on both directions. This
            ' enables the client to do I/O on its connection as if it were directly
            ' connected to the application server.

            ' create an 8 byte response array  
            Dim response As Byte() = New Byte(7) {}

            ' read the resonse from the network stream
            proxy.Read(response, 0, 8)

            '  evaluate the reply code for an error condition
            If response(1) <> SOCKS4_CMD_REPLY_REQUEST_GRANTED Then
                HandleProxyCommandError(response, destinationHost, destinationPort)
            End If
        End Sub

        ''' <summary>
        ''' Translate the host name or IP address to a byte array.
        ''' </summary>
        ''' <param name="destinationHost">Host name or IP address.</param>
        ''' <return>Byte array representing IP address in bytes.</return>
        Friend Function GetIPAddressBytes(ByVal destinationHost As String) As Byte()
            Dim ipAddr As IPAddress = Nothing

            '  if the address doesn't parse then try to resolve with dns
            If Not IPAddress.TryParse(destinationHost, ipAddr) Then
                Try
                    ipAddr = Dns.GetHostEntry(destinationHost).AddressList(0)
                Catch ex As Exception
                    Throw New ProxyException(String.Format(CultureInfo.InvariantCulture, "A error occurred while attempting to DNS resolve the host name {0}.", destinationHost), ex)
                End Try
            End If

            ' return address bytes
            Return ipAddr.GetAddressBytes()
        End Function

        ''' <summary>
        ''' Translate the destination port value to a byte array.
        ''' </summary>
        ''' <param name="value">Destination port.</param>
        ''' <return>Byte array representing an 16 bit port number as two bytes.</return>
        Friend Function GetDestinationPortBytes(ByVal value As Integer) As Byte()
            Dim array As Byte() = New Byte(1) {}
            array(0) = Convert.ToByte(value \ 256)
            array(1) = Convert.ToByte(value Mod 256)
            Return array
        End Function

        ''' <summary>
        ''' Receive a byte array from the proxy server and determine and handle and errors that may have occurred.
        ''' </summary>
        ''' <param name="response">Proxy server command response as a byte array.</param>
        ''' <param name="destinationHost">Destination host.</param>
        ''' <param name="destinationPort">Destination port number.</param>
        Friend Sub HandleProxyCommandError(ByVal response As Byte(), ByVal destinationHost As String, ByVal destinationPort As Integer)

            If response Is Nothing Then
                Throw New ArgumentNullException("response")
            End If

            '  extract the reply code
            Dim replyCode As Byte = response(1)

            '  extract the ip v4 address (4 bytes)
            Dim ipBytes As Byte() = New Byte(3) {}
            For i As Integer = 0 To 3
                ipBytes(i) = response(i + 4)
            Next

            '  convert the ip address to an IPAddress object
            Dim ipAddr As New IPAddress(ipBytes)

            '  extract the port number big endian (2 bytes)
            Dim portBytes As Byte() = New Byte(1) {}
            portBytes(0) = response(3)
            portBytes(1) = response(2)
            Dim port As Int16 = BitConverter.ToInt16(portBytes, 0)

            ' translate the reply code error number to human readable text
            Dim proxyErrorText As String
            Select Case replyCode
                Case SOCKS4_CMD_REPLY_REQUEST_REJECTED_OR_FAILED
                    proxyErrorText = "connection request was rejected or failed"
                    Exit Select
                Case SOCKS4_CMD_REPLY_REQUEST_REJECTED_CANNOT_CONNECT_TO_IDENTD
                    proxyErrorText = "connection request was rejected because SOCKS destination cannot connect to identd on the client"
                    Exit Select
                Case SOCKS4_CMD_REPLY_REQUEST_REJECTED_DIFFERENT_IDENTD
                    proxyErrorText = "connection request rejected because the client program and identd report different user-ids"
                    Exit Select
                Case Else
                    proxyErrorText = String.Format(CultureInfo.InvariantCulture, "proxy client received an unknown reply with the code value '{0}' from the proxy destination", replyCode.ToString(CultureInfo.InvariantCulture))
                    Exit Select
            End Select

            '  build the exeception message string
            Dim exceptionMsg As String = String.Format(CultureInfo.InvariantCulture, "The {0} concerning destination host {1} port number {2}.  The destination reported the host as {3} port {4}.", proxyErrorText, destinationHost, destinationPort, ipAddr.ToString(), _
             port.ToString(CultureInfo.InvariantCulture))

            '  throw a new application exception 
            Throw New ProxyException(exceptionMsg)
        End Sub

        Friend Sub WaitForData(ByVal stream As NetworkStream)
            Dim sleepTime As Integer = 0
            While Not stream.DataAvailable
                Thread.Sleep(WAIT_FOR_DATA_INTERVAL)
                sleepTime += WAIT_FOR_DATA_INTERVAL
                If sleepTime > WAIT_FOR_DATA_TIMEOUT Then
                    Throw New ProxyException("A timeout while waiting for the proxy destination to respond.")
                End If
            End While
        End Sub


#Region "Async Methods"

        Private _asyncWorker As BackgroundWorker
        Private _asyncException As Exception
        Private _asyncCancelled As Boolean

        ''' <summary>
        ''' Gets a value indicating whether an asynchronous operation is running.
        ''' </summary>
        ''' <remarks>return true if an asynchronous operation is running; otherwise, false.
        ''' </remarks>
        Public ReadOnly Property IsBusy() As Boolean
            Get
                Return If(_asyncWorker Is Nothing, False, _asyncWorker.IsBusy)
            End Get
        End Property

        ''' <summary>
        ''' Gets a value indicating whether an asynchronous operation is cancelled.
        ''' </summary>
        ''' <remarks>return true if an asynchronous operation is cancelled; otherwise, false.
        ''' </remarks>
        Public ReadOnly Property IsAsyncCancelled() As Boolean
            Get
                Return _asyncCancelled
            End Get
        End Property

        ''' <summary>
        ''' Cancels any asychronous operation that is currently active.
        ''' </summary>
        Public Sub CancelAsync()
            If _asyncWorker IsNot Nothing AndAlso Not _asyncWorker.CancellationPending AndAlso _asyncWorker.IsBusy Then
                _asyncCancelled = True
                _asyncWorker.CancelAsync()
            End If
        End Sub

        Private Sub CreateAsyncWorker()
            If _asyncWorker IsNot Nothing Then
                _asyncWorker.Dispose()
            End If
            _asyncException = Nothing
            _asyncWorker = Nothing
            _asyncCancelled = False
            _asyncWorker = New BackgroundWorker()
        End Sub

        ''' <summary>
        ''' Event handler for CreateConnectionAsync method completed.
        ''' </summary>
        Public Event CreateConnectionAsyncCompleted As EventHandler(Of CreateConnectionAsyncCompletedEventArgs) Implements SFDL.Proxy.Proxy.IProxyClient.CreateConnectionAsyncCompleted

        ''' <summary>
        ''' Asynchronously creates a remote TCP connection through a proxy server to the destination host on the destination port
        ''' using the supplied open TcpClient object with an open connection to proxy server.
        ''' </summary>
        ''' <param name="destinationHost">Destination host name or IP address.</param>
        ''' <param name="destinationPort">Port number to connect to on the destination host.</param>
        ''' <remarks>
        ''' This instructs the proxy server to make a pass through connection to the specified destination host on the specified
        ''' port.  
        ''' </remarks>
        Public Sub CreateConnectionAsync(ByVal destinationHost As String, ByVal destinationPort As Integer) Implements SFDL.Proxy.Proxy.IProxyClient.CreateConnectionAsync
            If _asyncWorker IsNot Nothing AndAlso _asyncWorker.IsBusy Then
                Throw New InvalidOperationException("The Socks4/4a object is already busy executing another asynchronous operation.  You can only execute one asychronous method at a time.")
            End If

            CreateAsyncWorker()
            _asyncWorker.WorkerSupportsCancellation = True
            AddHandler _asyncWorker.DoWork, New DoWorkEventHandler(AddressOf CreateConnectionAsync_DoWork)
            AddHandler _asyncWorker.RunWorkerCompleted, New RunWorkerCompletedEventHandler(AddressOf CreateConnectionAsync_RunWorkerCompleted)
            Dim args As [Object]() = New [Object](1) {}
            args(0) = destinationHost
            args(1) = destinationPort
            _asyncWorker.RunWorkerAsync(args)
        End Sub

        Private Sub CreateConnectionAsync_DoWork(ByVal sender As Object, ByVal e As DoWorkEventArgs)
            Try
                Dim args As [Object]() = DirectCast(e.Argument, [Object]())
                e.Result = CreateConnection(DirectCast(args(0), String), CInt(args(1)))
            Catch ex As Exception
                _asyncException = ex
            End Try
        End Sub

        Private Sub CreateConnectionAsync_RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs)
            RaiseEvent CreateConnectionAsyncCompleted(Me, New CreateConnectionAsyncCompletedEventArgs(_asyncException, _asyncCancelled, DirectCast(e.Result, TcpClient)))
        End Sub

#End Region

    End Class

End Namespace